package ml.puredark.hviewer.ui.customs;

import android.support.annotation.Nullable;

import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.internal.Supplier;
import com.facebook.datasource.AbstractDataSource;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSubscriber;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.NotThreadSafe;
import net.jcip.annotations.ThreadSafe;

import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;


@NotThreadSafe
public class RetainingDataSourceSupplier<T> implements Supplier<DataSource<T>> {

    private final Set<RetainingDataSource> mDataSources =
            Collections.newSetFromMap(new WeakHashMap<RetainingDataSource, Boolean>());

    private Supplier<DataSource<T>> mCurrentDataSourceSupplier = null;

    @Override
    public DataSource<T> get() {
        RetainingDataSource dataSource = new RetainingDataSource();
        dataSource.setSupplier(mCurrentDataSourceSupplier);
        mDataSources.add(dataSource);
        return dataSource;
    }

    public void setSupplier(Supplier<DataSource<T>> supplier) {
        mCurrentDataSourceSupplier = supplier;
        for (RetainingDataSource dataSource : mDataSources) {
            dataSource.setSupplier(supplier);
        }
    }

    @ThreadSafe
    private class RetainingDataSource extends AbstractDataSource<T> {
        @GuardedBy("RetainingDataSource.this")
        @Nullable
        private DataSource<T> mDataSource = null;

        public void setSupplier(@Nullable Supplier<DataSource<T>> supplier) {
            // early return without calling {@code supplier.get()} in case we are closed
            if (isClosed()) {
                return;
            }
            DataSource<T> oldDataSource;
            DataSource<T> newDataSource = (supplier != null) ? supplier.get() : null;
            synchronized (RetainingDataSource.this) {
                if (isClosed()) {
                    oldDataSource = newDataSource;
                    newDataSource = null;
                } else {
                    oldDataSource = mDataSource;
                    mDataSource = newDataSource;
                }
            }
            if (newDataSource != null) {
                newDataSource.subscribe(new InternalDataSubscriber(), CallerThreadExecutor.getInstance());
            }
            closeSafely(oldDataSource);
        }

        @Override
        @Nullable
        public synchronized T getResult() {
            return (mDataSource != null) ? mDataSource.getResult() : null;
        }

        @Override
        public synchronized boolean hasResult() {
            return (mDataSource != null) && mDataSource.hasResult();
        }

        @Override
        public boolean close() {
            DataSource<T> dataSource;
            synchronized (RetainingDataSource.this) {
                // it's fine to call {@code super.close()} within a synchronized block because we don't
                // implement {@link #closeResult()}, but perform result closing ourselves.
                if (!super.close()) {
                    return false;
                }
                dataSource = mDataSource;
                mDataSource = null;
            }
            closeSafely(dataSource);
            return true;
        }

        private void onDataSourceNewResult(DataSource<T> dataSource) {
            if (dataSource == mDataSource) {
                setResult(null, false);
            }
        }

        private void onDataSourceFailed(DataSource<T> dataSource) {
            // do not propagate failure
        }

        private void onDatasourceProgress(DataSource<T> dataSource) {
            if (dataSource == mDataSource) {
                setProgress(dataSource.getProgress());
            }
        }

        private void closeSafely(DataSource<T> dataSource) {
            if (dataSource != null) {
                dataSource.close();
            }
        }

        private class InternalDataSubscriber implements DataSubscriber<T> {
            @Override
            public void onNewResult(DataSource<T> dataSource) {
                if (dataSource.hasResult()) {
                    RetainingDataSource.this.onDataSourceNewResult(dataSource);
                } else if (dataSource.isFinished()) {
                    RetainingDataSource.this.onDataSourceFailed(dataSource);
                }
            }

            @Override
            public void onFailure(DataSource<T> dataSource) {
                RetainingDataSource.this.onDataSourceFailed(dataSource);
            }

            @Override
            public void onCancellation(DataSource<T> dataSource) {
            }

            @Override
            public void onProgressUpdate(DataSource<T> dataSource) {
                RetainingDataSource.this.onDatasourceProgress(dataSource);
            }
        }
    }
}